#pragma once
/*
    Name: Simple INI Parser
    Author: Kim C. Yem
    Version: 1.0
    License: Apache License 2.0

    Description:
        A string grouping container that can read/write to files in INI format

    Features:
        - Unique grouping of entries completely compartmentalized from each other
        - Seamless handling between unique and nonunique entry names
        - Single header only file for ease of intergration

    Issues:
        - No support for unicode string, will crash (currently due to trim function i think)

    Future?
        - Multiline value support
        - utf8 support for cIni
        - create wIni which is a wide char version for utf16
*/


#include <string>
#include <map>
#include <vector>
#include <fstream>

#include <locale>   //For trim


namespace util
{
    /// trim from start
    static inline std::string ltrim(std::string& str, const std::locale& loc = std::locale()) {
        std::string::size_type pos = 0;
        while( pos < str.size() && std::isspace(str[pos], loc) )
            pos++;
        str.erase(0, pos);
        return str;
    }

    /// trim from end
    static inline std::string rtrim(std::string& str, const std::locale& loc = std::locale()) {
        std::string::size_type pos = str.size();
        while( pos > 0 && std::isspace(str[pos - 1], loc) )
            pos--;
        str.erase(pos);
        return str;
    }
}
using util::ltrim;
using util::rtrim;

class cIni
{
    private:   //Private typedef
        typedef std::string t_string;

        typedef t_string t_section;
        typedef t_string t_key;
        typedef t_string t_value;

        typedef std::multimap<t_key, t_value> t_entrymap;
        typedef std::map<t_section, t_entrymap*> t_sectionmap;

        typedef t_entrymap::iterator t_entryit;
        typedef t_sectionmap::iterator t_sectionit;

        typedef std::pair<t_entryit,t_entryit> t_rangeit;

    public:   //Public Typedef
        typedef std::vector<t_string> t_list;   //! List of entry values
        class t_entry {   //! Entry structure
            public:
                t_entry( t_key k, t_value v ) : key(k), value(v) {};
                t_key key;
                t_value value;
        };
        typedef std::vector<t_entry> t_entrylist;   //! List of entry values

    private:   //Internal Variables
        t_sectionmap sectionmap;
        t_entrymap rootmap;

        t_entrymap* currentGroup;   //! Internal group pointer

    public:   //Public Interface
        bool parse(const std::string& filename);   //! load file into memory, returns false if file doesn't exists or can't be read
        void save(const std::string& filename);   //! save memory into file
        void clear();   //! clear memory

        bool select(const t_section& section = "");   //! Set section group; if group doesn't exists, autocreate group and returns false

        t_list getGroupNames();   //! Get list of existing group names
        t_list getEntryNames();   //! Get list of existing entry names, duplicates are removed
        bool exists(const t_key& key);   //! If any extry exists

        t_value& operator[](const t_key& key);   //! Single entry set and get, grabs first found, autocreates if doesn't exists

        t_value get(const t_key& key, const t_value& def = "");   //! Single entry set with default value, grabs first found, does not autocreate but allows return of default value
        t_list getList(const t_key& key);   //! Get list of value under key, returns a empty list if entry doesn't exists
        t_entrylist getList();   //! Get list of entries under current selected group, returns a empty list if group is empty
        t_entrylist getAll();   //! Get all entries that exists under all groups
        t_entrylist getSubKeyList(const t_key& key);   //! Get subgroup list (subgroup.key=value); relatively expensive operation so avoid for large groups

        void set(const t_key& key, const t_value& value);   //! Clears original entries and set single value
        void set(const t_key& key, const t_list& values);   //! Clears original entries and set list of value under key,
        void add(const t_key& key, const t_value& value);   //! Add entry without removing previous entries
        void erase(const t_key& key);   //! erase all entries under key

    public:  //Ctor & dtor
        cIni() { select(""); }
        ~cIni() { clear(); }

        cIni(const cIni& object);
        cIni& operator= (const cIni& object);

};


inline cIni::cIni(const cIni& object)
{
    this->rootmap = object.rootmap;

    for( t_sectionit oit = (t_sectionit&)object.sectionmap.begin(); oit != object.sectionmap.end(); ++oit )
    {
        std::pair<t_sectionit,bool> it = this->sectionmap.insert( std::pair<t_section,t_entrymap*>( oit->first, new t_entrymap ) );
        *(it.first->second) = *(oit->second);   //Copy entrymap from object to new entrymap
    }
}

inline cIni& cIni::operator=(const cIni& object)
{
    this->rootmap = object.rootmap;

    for( t_sectionit oit = (t_sectionit&)object.sectionmap.begin(); oit != object.sectionmap.end(); ++oit )
    {
        std::pair<t_sectionit,bool> it = this->sectionmap.insert( std::pair<t_section,t_entrymap*>( oit->first, new t_entrymap ) );
        *(it.first->second) = *(oit->second);   //Copy entrymap from object to new entrymap
    }
    return (*this);
}


inline bool cIni::parse(const std::string& filename)
{
    clear();
    std::string line;

    std::ifstream file( filename.c_str() );
    if( file.is_open() )
    {
        while( !file.eof( ) )
        {
            if( std::getline(file, line) )
            {
                std::size_t pos = line.find_first_of('#');
                if( pos != std::string::npos )
                line = line.substr(0,pos);

                ltrim(line);

                if( !line.empty() )
                {
                    rtrim(line);
                    if ( line[0] == '[' )  //Set Section
                    {
                        pos = line.find_first_of(']');
                        if( pos > 2 )
                            select( line.substr(1,pos-1) );
                    }
                    else //Set Entry
                    {
                        pos = line.find('=');
                        if( currentGroup != NULL && pos != std::string::npos )
                        {
                            add( rtrim(line.substr(0,pos)) , ltrim(line.substr(pos+1,line.length())) );
                        }
                    }
                }
            }
            else
                ;   //TODO: Add debugging message
        }
        file.close();
        select("");
        return true;
    }
    else
        return false;
}


inline void cIni::save(const std::string& filename)
{
    std::ofstream file(filename.c_str(), std::ofstream::trunc);

    //Loop through root
    for( t_entryit i = rootmap.begin(); i != rootmap.end(); ++ i )
    {
        //Write entry (key = value)
        file << i->first << " = " << i->second << "\n";
    }


    //Loop through sections
    for(t_sectionit i = sectionmap.begin(); i != sectionmap.end(); ++i)
    {
        if( i->second->empty() )  //No entry in section, skip to next
            continue;

        //Write section
        file << "\n[" << i->first << "]\n";

        for(t_entryit j = i->second->begin(); j != i->second->end(); ++j)
        {
            //Write entry (key = value)
            file << j->first << " = " << j->second << "\n";
        }
    }

    file.close();
}


inline void cIni::clear()
{
    //{ Delete all the dynamically allocated entrymaps in the sectionmap
        t_sectionit it = sectionmap.begin();
        t_sectionit end = sectionmap.end();
        while( it != end )
        {
            delete (*it).second;
            ++it;
        }
    //}
    sectionmap.clear();
    rootmap.clear();

    select("");
}


inline bool cIni::select(const t_section& section)
{
    if( section.empty() )
    {
        currentGroup = &rootmap;
        return true;
    }

    t_sectionit it = sectionmap.find(section);
    if( it == sectionmap.end() )
    {
        currentGroup = new t_entrymap;
        sectionmap.insert( std::pair<t_section,t_entrymap*>(section,currentGroup) );
        return false;
    }
    else
    {
        currentGroup = it->second;
        return true;
    }
}


inline cIni::t_list cIni::getGroupNames()
{
    t_list temp;

    for( t_sectionit it = sectionmap.begin(); it != sectionmap.end(); ++it )
        temp.push_back( it->first );

    return temp;
}

inline cIni::t_list cIni::getEntryNames()
{
    t_list temp;

    for( t_entryit it = currentGroup->begin(); it != currentGroup->end(); ++it )
        temp.push_back(it->first);

    std::sort( temp.begin(), temp.end() );
    temp.erase( std::unique( temp.begin(), temp.end() ), temp.end() );

    return temp;
}

inline cIni::t_value& cIni::operator[](const t_key& key)
{
    t_rangeit ret = currentGroup->equal_range(key);

    if( ret.first != currentGroup->end() )
    {
        if( ret.first == ret.second )
            return ret.first->second;
        else
        {

            t_value temp = currentGroup->find(key)->second;
            currentGroup->erase( ret.first, ret.second );
            return currentGroup->insert( std::pair<t_key,t_value>(key,temp) )->second;
        }
    }
    else
        return currentGroup->insert( std::pair<t_key,t_value>(key,"") )->second;
}


inline bool cIni::exists(const t_key& key)
{
    if( currentGroup->find(key) == currentGroup->end() )
        return false;
    else
        return true;
}


inline cIni::t_value cIni::get(const t_key& key, const t_value& def)
{
    t_entryit temp = currentGroup->find(key);
    if( temp == currentGroup->end() )
    {
        add(key,def);
        return def;
    }
    return temp->second;
}

inline cIni::t_list cIni::getList(const t_key& key)
{
    t_list temp;

    t_rangeit ret = currentGroup->equal_range(key);
    for( t_entryit it = ret.first; it != ret.second; ++it )
        temp.push_back( it->second );

    return temp;
}

inline cIni::t_entrylist cIni::getList()
{
    t_entrylist temp;

    for( t_entryit it = currentGroup->begin(); it != currentGroup->end(); ++it )
        temp.push_back( t_entry(it->first,it->second) );

    return temp;
}

inline cIni::t_entrylist cIni::getAll()
{
    t_entrylist temp;

    for( t_sectionit groupit = sectionmap.begin(); groupit != sectionmap.end(); ++groupit )
        for( t_entryit it = groupit->second->begin(); it != groupit->second->end(); ++it )
            temp.push_back( t_entry(it->first,it->second) );

    return temp;
}

inline cIni::t_entrylist cIni::getSubKeyList(const t_key& key)
{
    t_entrylist temp;

    for( t_entryit it = currentGroup->begin(); it != currentGroup->end(); ++it )
    {
        std::size_t pos = it->first.find_first_of('.');
        if( pos > 0 && it->first.substr( 0, pos ) == key )
        {
            temp.push_back( t_entry( it->first.substr( pos+1 ), it->second ) );
        }
    }

    return temp;
}


inline void cIni::erase(const t_key& key)
{
    t_rangeit ret = currentGroup->equal_range(key);

    currentGroup->erase( ret.first, ret.second );
}

inline void cIni::set(const t_key& key, const t_list& values)
{
    erase(key);

    int max = values.size();
    for( int i=0; i < max; ++i )
    {
        add(key,values[i]);
    }
}


inline void cIni::set(const t_key& key, const t_value& value)
{
    erase(key);

    add(key,value);
}

inline void cIni::add(const t_key& key, const t_value& value)
{
    currentGroup->insert( std::pair<t_key,t_value>(key,value) );
}
